/**
 * LangGraph.js 入门示例
 *
 * 本文件展示了如何使用LangGraph.js创建一个简单的AI代理，该代理能够：
 * 1. 接收用户问题
 * 2. 使用大语言模型(LLM)处理问题
 * 3. 根据需要调用外部工具(如网络搜索)获取信息
 * 4. 返回最终答案
 *
 * LangGraph核心概念解析：
 * - 状态图(StateGraph): LangGraph的核心，用于定义工作流程
 * - 节点(Node): 图中的处理单元，执行特定功能(如调用模型、执行工具)
 * - 边(Edge): 连接节点，定义工作流的执行路径
 * - 条件边(ConditionalEdge): 根据条件动态决定下一步执行路径
 * - 状态(State): 在节点间传递的数据，包含对话历史等信息
 *
 * 工作流程：
 * 1. 用户输入问题
 * 2. 调用模型节点处理问题
 * 3. 路由函数检查模型是否需要使用工具
 * 4. 如需使用工具，执行工具节点，然后返回模型节点继续处理
 * 5. 如不需使用工具，返回最终答案
 *
 * 这种循环执行的模式使AI代理能够根据需要多次使用工具，直到解决问题。
 */

// 设置Tavily API密钥，用于网络搜索功能
// process.env.TAVILY_API_KEY = 'tvly-dev-xxx';

// // 设置OpenAI相关配置
// // 这里使用的是阿里云的通义千问模型(qwen-plus)
// process.env.OPENAI_MODEL_NAME = 'qwen-plus';
// process.env.OPENAI_API_KEY = 'sk-xxx';
// // 设置基础URL为阿里云兼容模式API
// process.env.OPENAI_BASE_URL =
//   'https://dashscope.aliyuncs.com/compatible-mode/v1';

import '../lib/utils/loadEnv';
// 导入Tavily搜索工具
// 这是一个网络搜索工具，可以让AI代理获取实时信息
import { TavilySearch } from '@langchain/tavily';

// 导入消息类型，用于处理AI和人类之间的对话
import { AIMessage, HumanMessage } from '@langchain/core/messages';
// 导入可运行配置类型，用于配置LangGraph组件
import { RunnableConfig } from '@langchain/core/runnables';
// 导入LangGraph核心组件：消息注解和状态图
// MessagesAnnotation用于管理对话历史
// StateGraph是LangGraph的核心，用于定义工作流
import {
  END,
  MessagesAnnotation,
  START,
  StateGraph,
} from '@langchain/langgraph';
// 导入工具节点，这是一个预构建的节点，用于处理工具调用
import { ToolNode } from '@langchain/langgraph/prebuilt';

// 导入OpenAI聊天模型，用于与大语言模型交互
import { ChatOpenAI } from '@langchain/openai';

/**
 * 定义代理的可配置参数。
 * 这里我们设置了代理可以配置的参数，包括系统提示词和使用的模型。
 */
import { Annotation } from '@langchain/langgraph';

// 系统提示词模板，用于指导AI助手的行为
// 这是发送给模型的初始指令，定义了它的角色和行为方式
const SYSTEM_PROMPT_TEMPLATE = `你是一个有帮助的AI助手。你将收到一个问题或任务，你应该提供最准确和最有帮助的信息。
`;

console.log('%c Line:72 🍊', 'color:#3f7cff', '开始');
// 导出配置模式，使其可以在其他地方使用
// 这定义了代理的可配置参数结构
export const ConfigurationSchema = Annotation.Root({
  /**
   * 代理使用的系统提示词。
   * 系统提示词用于指导AI助手的行为和回答方式。
   */
  systemPromptTemplate: Annotation<string>,

  /**
   * 代理使用的语言模型名称。
   * 不同的模型有不同的能力和特点，可以根据需求选择合适的模型。
   */
  model: Annotation<string>,
});

function ensureConfiguration(
  config: RunnableConfig
): typeof ConfigurationSchema.State {
  /**
   * 确保默认配置被正确填充。
   * 如果用户没有提供某些配置项，我们使用预设的默认值。
   */
  const configurable = config.configurable ?? {};
  return {
    systemPromptTemplate:
      configurable.systemPromptTemplate ?? SYSTEM_PROMPT_TEMPLATE,
    model: configurable.model ?? 'claude-3-7-sonnet-latest',
  };
}

// 主函数，用于设置和运行LangGraph工作流
async function run() {
  // 确保使用最新版本的ChatOpenAI，支持工具调用
  // 工具调用是LLM的一个重要特性，允许模型决定何时使用外部工具

  // 创建Tavily搜索工具实例，设置最多返回3个结果
  const agentTools = [new TavilySearch({ maxResults: 3 })];
  // 定义调用模型的函数
  // 这个函数是LangGraph中的一个节点，负责调用语言模型并处理响应
  async function callModel(
    state: typeof MessagesAnnotation.State,
    config: RunnableConfig
  ): Promise<typeof MessagesAnnotation.Update> {
    /** 调用为我们的代理提供支持的大语言模型(LLM) **/
    const configuration = ensureConfiguration(config);

    // 创建一个新的ChatOpenAI实例并绑定工具
    // bindTools方法使模型能够使用我们定义的工具
    // 这是LangGraph中实现工具使用的关键步骤
    const model = new ChatOpenAI({
      modelName: 'qwen-plus', // 使用通义千问模型
      temperature: 0, // 设置为0以获得确定性输出
    }).bindTools(agentTools ?? []); // 绑定工具，如果agentTools为undefined则使用空数组

    // console.log("%c Line:38 🍑 config", "color:#ea7e5c", config);

    // 调用模型并获取响应
    // 这里我们传入系统提示词和当前的消息历史
    const response = await model.invoke(
      [
        // 系统消息，设置模型的行为和角色
        {
          role: 'system',
          content: configuration.systemPromptTemplate.replace(
            '{system_time}', // 替换模板中的时间占位符
            new Date().toISOString() // 使用当前时间的ISO字符串格式
          ),
        },
        ...state.messages, // 展开当前的消息历史
      ],
      config // 传入配置
    );

    // 返回模型的响应作为状态更新
    // 在LangGraph中，节点函数需要返回状态的更新部分
    return { messages: [response] }; // 将模型响应添加到消息历史中
  }

  // 定义确定下一步路由的函数
  // 这个函数决定了工作流的流向，根据模型输出决定下一步操作
  // 路由函数：决定工作流的下一步去向
  function routeModelOutput(state: typeof MessagesAnnotation.State): string {
    const messages = state.messages; // 获取当前的消息历史
    const lastMessage = messages[messages.length - 1]; // 获取最后一条消息（模型的响应）

    // 检查模型是否请求使用工具
    // tool_calls是OpenAI格式的工具调用请求
    // 如果存在工具调用请求，则路由到工具节点
    if ((lastMessage as AIMessage)?.tool_calls?.length || 0 > 0) {
      return 'tools'; // 返回'tools'表示下一步执行工具节点
    }
    // 如果没有工具调用请求，则结束图的执行
    // 这意味着模型已经给出了最终回答
    else {
      return END; // 特殊标记，表示工作流结束
    }
  }

  // 最后，我们编译图！
  // 这将工作流编译为一个可调用和部署的图。
  // 编译后的图可以被调用来处理用户输入并生成响应
  // 创建并编译工作流图
  // 这是LangGraph的核心部分，定义了节点和边的连接关系
  // 使用立即执行的异步函数创建和编译图
  // 这种模式允许我们在异步环境中构建图
  const graph = await (async () => {
    // 定义一个新的图。我们使用预构建的MessagesAnnotation来定义状态：
    // 这是LangGraph中管理消息历史的标准方式
    // 详细文档：https://langchain-ai.github.io/langgraphjs/concepts/low_level/#messagesannotation
    const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema)
      // 定义我们将在其间循环的两个节点
      // 节点是图中的基本处理单元，每个节点执行特定的功能
      .addNode('callModel', callModel)
      .addNode('tools', new ToolNode(agentTools))
      // 将入口点设置为`callModel`
      // 这意味着这个节点是第一个被调用的
      // 工作流从这个节点开始执行
      .addEdge(START, 'callModel')
      .addConditionalEdges(
        // 首先，我们定义边的源节点。我们使用`callModel`。
        // 这意味着这些边是在`callModel`节点被调用后采取的路径。
        'callModel',
        // 接下来，我们传入一个函数，该函数将确定目标节点，
        // 这些节点将在源节点被调用后被调用。
        // 这实现了动态路由，根据模型输出决定下一步
        routeModelOutput
      )
      // 这意味着在`tools`节点被调用后，下一个调用的是`callModel`节点
      // 形成了一个循环：模型调用->工具执行->模型调用->...
      .addEdge('tools', 'callModel');
    // 编译工作流，将其转换为可执行的图
    // 编译过程会验证图的结构，确保所有节点和边都正确连接
    return workflow.compile({
      interruptBefore: [], // 如果你想在调用工具前更新状态，可以在这里设置中断点
      interruptAfter: [], // 如果你想在调用工具后更新状态，可以在这里设置中断点
      // 中断点允许在工作流执行过程中插入自定义逻辑
    });
  })();

  const md = (await graph.getGraphAsync()).drawMermaid();
  console.log('%c Line:221 🍋 md\nn', 'color:#42b983', md);

  // 调用编译后的图，传入用户问题
  // invoke方法启动工作流的执行
  const res = await graph.invoke({
    messages: [new HumanMessage('上海天气怎么样')], // 创建一个人类消息作为输入
  });

  // 打印结果，可以看到完整的对话历史和最终回答
  // 结果包含了整个对话过程，包括工具调用和最终回答
  console.log('%c Line:160 🥕 res', 'color:#2eafb0', res);
}

// 运行主函数并捕获可能的错误
run().catch((err) => {
  console.error('运行代理时出错:', err);
  process.exit(1); // 出错时退出程序
});
